單看標題,是不是會想這有什麼好講的?就在圖片加上 width: 100%; height:100%; object-fit: cover 不就好了。
是這樣沒錯,但不是這樣的。
先來段小劇場
( ・∀・)σ < 這邊要顯示使用者自行上傳的圖片,圖片會完整呈現。
好,沒問題啊!> d(・∀・ )
( ・∀・)σ < 圖片要加個圓角跟框框。
好,沒問題啊!但會有空白喔(object-fit: contain)> d(・∀・ )
( ‘・ω・ˋ)σ < 我想讓邊框貼齊圖片邊緣耶。
好…沒問題啊!我試試看!> d(・∀・ )
=========== A Few Minutes Later ===========
發現還要兼顧 RWD,不超出外容器的同時,還要置中。表情逐漸母湯 → (゚д ゚ ; )
.
一開始是個很簡單的畫面需求,為什麼到實際製作時,會變成很複雜呢?首先,來看看有哪些要克服的問題點。
object-fit,因為 contain 會留空白、cover 有機會大片裁切圖片。藉由上方的分析,我們大概知道需要克服哪些事情,以及製作時需要哪些資訊,包含:
💡 今天的實作會使用到 Vue。考慮到讀者可能對 Vue 比較不熟悉,簡單介紹一下使用到的語法。
ref(script 內):建立響應式變數,用.value讀寫,變動時畫面自動更新。ref="x"(template 內):取得該 DOM 元素,存入同名的 ref 變數。::屬性綁定,把變數或運算式傳給屬性,如:src、:style。@:事件監聽,如@click、@load。
首先是 HTML 結構,需要三層:
ResizeObserver 監聽空間的變化<template>
<div>
<!-- 第一層 -->
<section class="image-container" ref="imageContainerRef">
<!-- 第二層 -->
<div class="image-frame" :style="frameStyle">
<!-- 第三層 -->
<img :src="imageUrl" alt="示範圖片" ref="imgElementRef" @load="onImageLoad" />
</div>
</section>
...
</div>
</template>
然後,設定 style
/* 第一層:填滿可用空間,flex 置中 */
.image-container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
/* 第二層:外觀設定 */
.image-frame {
overflow: hidden;
border: 2px solid #fff;
border-radius: 16px;
box-shadow: 0 4px 14px rgb(0 0 0 / 0.25);
transition: width 0.25s ease-in-out, height 0.25s ease-in-out;
}
/* 第三層:填滿 frame;因為 frame 已與圖片同比例,所以 cover 不會裁切 */
.image-frame img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
接下來,重點來了!
我們要用 JS 來計算、監聽畫面上的 DOM,並且將計算結果寫入 .image-frame 那層的 style 屬性中。考慮到 resize 的過渡,寬高設定是用 % 為單位。這樣在 debounce 重算前,框也不會超出容器。
import { ref, onMounted, onUnmounted } from 'vue'
const imageContainerRef = ref(null)
const imgElementRef = ref(null)
const frameStyle = ref({ width: '80%', height: '80%' })
const imageUrl = ref('上傳圖片的連結')
// 將計算的 px 轉成相對單位
const pxToPercent = (value, denominator) => (value / denominator) * 100 + '%'
// 核心 function
const calculateSize = () => {
const img = imgElementRef.value
const container = imageContainerRef.value
if (!img || !container) return
// 取得圖片的原始寬高,以及 container 的寬高
const { naturalWidth: imgW, naturalHeight: imgH } = img
const { clientWidth: containerW, clientHeight: containerH } = container
if (!containerW || !containerH || !imgW || !imgH) return
// 算出原始寬高比與容器寬高比
const imgRatio = imgW / imgH
const containerRatio = containerW / containerH
if (imgRatio > containerRatio) {
// 圖片比容器「更寬」:寬度撐滿,高度依比例縮小
frameStyle.value.width = '100%'
frameStyle.value.height = pxToPercent(containerW / imgRatio, containerH)
} else {
// 圖片比容器「更窄」:高度撐滿,寬度依比例縮小
frameStyle.value.width = pxToPercent(containerH * imgRatio, containerW)
frameStyle.value.height = '100%'
}
}
// 圖片載入(含切換圖片)後立即計算一次
const onImageLoad = () => calculateSize()
// 加上 debounce 與監聽容器大小變動
const debounce = (fn, delay) => {
let timer = null
return (...args) => {
clearTimeout(timer)
timer = setTimeout(() => fn(...args), delay)
}
}
const debouncedCalculateSize = debounce(calculateSize, 150)
let resizeObserver = null
onMounted(() => {
resizeObserver = new ResizeObserver(debouncedCalculateSize)
resizeObserver.observe(imageContainerRef.value)
})
onUnmounted(() => resizeObserver?.disconnect())
這樣就完成啦!是不是非常簡單呢?

.
.
.

其實,我有點驚訝。隨著文章到收尾階段,心中冒出「就這樣?!」的驚嘆。
當初開發的時候,花了非常多心思在思考如何處理畫面。踩了很多坑,多次調整寫法。現在拿出來再看,怎麼這麼簡單?!冷靜後仔細一想,這是因為文章一開始就把會碰到的問題先列出來。由問題出發,就能夠更清楚該怎麼規劃,像是三層的 HTML 結構、frame 的計算方式等等。
這次藉由參加鐵人賽,回顧之前寫的 code。在意料之外的地方也獲得了收穫。
明天,我們將進入新的子題目 — SVG。
つづく